import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Hashtable;

/*
 * Class Lexer
 * 
 * Update the method nextToken() such to the provided
 * specifications of the Decaf Programming Language.
 * 
 * You are not allowed to use any built it in tokenizer
 * in Java. You are only allowed to scan the input file
 * one character at a time.
 */

public class Lexer {

	private BufferedReader reader; // Reader
	private char curr; // The current character being scanned

	private Hashtable<String, String> keyWords;

	public void hashTableStart() {
		keyWords = new Hashtable<String, String>();
		keyWords.put("class", "KW");
		keyWords.put("else", "KW");
		keyWords.put("if", "KW");
		keyWords.put("int", "KW");
		keyWords.put("float", "KW");
		keyWords.put("boolean", "KW");
		keyWords.put("String", "KW");
		keyWords.put("return", "KW");
		keyWords.put("static", "KW");
		keyWords.put("while", "KW");
		keyWords.put("false", "BL");
		keyWords.put("true", "BL");

	}

	private static final char EOF = (char) (-1);

	// End of file character

	public Lexer(String file) {
		try {
			reader = new BufferedReader(new FileReader(file));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		// Read the first character
		curr = read();
	}

	private char read() {
		try {
			return (char) (reader.read());
		} catch (IOException e) {
			e.printStackTrace();
			return EOF;
		}
	}

	// Checks if a character is a digit
	private boolean isNumeric(char c) {
		if (c >= '0' && c <= '9')
			return true;

		return false;
	}

	public double power(int num, int power) {
		double result = 1;
		for (int i = 0; i < power; i++) {
			result *= num;
		}
		return result;
	}

	public boolean isLetter(char l) {
		if (l >= 'A' && l <= 'z') {
			return true;
		}
		return false;
	}

	public boolean whiteSpace(char w) {
		switch (w) {

		case ' ':
			return true; // Whitespaces
		case '\n':
			return true;
		case '\b':
			return true;
		case '\r':
			return true;
		case '\t':
			return true;
		case '\f':
			return true;
		default:
			return false;

		}
	}

	public Token nextToken() {

		hashTableStart();
		int state = 1; // Initial state
		int numBuffer = 0; // A buffer for number literals
		boolean decimalFlag = false;
		double decimalNumBuffer = 0;
		boolean comment = false;
		String stringBuffer = "";
		int i = 1;

		while (true) {
			if (curr == EOF) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
				return null;
			}

			switch (state) {
			// Controller
			case 1:
				switch (curr) {
				case ' ': // Whitespaces
				case '\n':
				case '\b':
				case '\r':
				case '\t':
				case '\f':
					curr = read();
					continue;

				case ';':
					curr = read();
					return new Token("SM", ";");

				case '+':
					curr = read();
					return new Token("PO", "+");

				case '-':
					curr = read();
					return new Token("MO", "-");

				case '*':
					curr = read();
					return new Token("TO", "*");

				case '/':
					curr = read();
					comment = false;
					state = 11;
					continue;
				case ',':
					curr = read();
					return new Token("FA", ",");
				case '(':
					curr = read();
					return new Token("LP", "(");
				case ')':
					curr = read();
					return new Token("RP", ")");
				case '{':
					curr = read();
					return new Token("LB", "{");
				case '}':
					curr = read();
					return new Token("RB", "}");
				case '%':
					curr = read();
					return new Token("MD", "%");
				case '.':
					return new Token("ERROR", "Invalid input " + ".");
				case '=':
					state = 5;
					continue;
				case '&':
					state = 6;
					continue;
				case '|':
					state = 7;
					continue;
				case '!':
					state = 8;
					continue;
				case '\"':
					stringBuffer = "\"";
					state = 2;
					continue;
				case '_':
					stringBuffer = "_";
					state = 9;
					continue;

				default:
					// Integer - Start
					if (isNumeric(curr)) {
						numBuffer = 0; // Reset the buffer.
						numBuffer += (curr - '0');

						state = 3;
						curr = read();
					} else if (isLetter(curr)) {
						stringBuffer = "" + curr;
						state = 10;
					} else {
						stringBuffer = curr + "";
						curr = read();
						return new Token("ERROR","Invalid input: " + stringBuffer);
					}
					continue;

				}

				// String
			case 2:
				curr = read();
				if (curr == '\"') {
					curr = read();
					return new Token("ST", stringBuffer + "\"");
				} else if (curr != '\n' && curr != '\r' && curr != EOF) {
					stringBuffer += curr;
					continue;

				} else {
					return new Token("ERROR", "Invalid String Literal");
				}

				// Integer - Body
			case 3:
				if (isNumeric(curr)) {
					numBuffer *= 10;
					numBuffer += (curr - '0');

					curr = read();
				} else if (curr == '.') {
					decimalNumBuffer = numBuffer;
					decimalFlag = false;
					i = 1;

					state = 4;
					curr = read();
				} else {
					return new Token("NM", "" + numBuffer);
				}
				continue;

				// Float - detected

			case 4:
				if (isNumeric(curr)) {
					int helper = curr - '0';
					decimalNumBuffer += (helper / power(10, i));
					decimalFlag = true;
					i++;

					curr = read();
				} else if (decimalFlag) {
					return new Token("NM", "" + decimalNumBuffer);
				} else {

					return new Token("ERROR", "invalid Input "
							+ decimalNumBuffer);

				}
				continue;

			case 5:
				curr = read();
				if (curr == '=') {
					curr = read();
					return new Token("EQ", "==");
				} else {
					return new Token("AO", "=");

				}

			case 6:
				curr = read();
				if (curr == '&') {
					curr = read();
					return new Token("LA", "&&");
				} else {
					return new Token("ERROR", "Invalid Input " + "&");
				}

			case 7:
				curr = read();
				if (curr == '|') {
					curr = read();
					return new Token("LO", "||");
				} else {
					return new Token("ERROR", "Invalid Input " + "|");
				}
			case 8:
				curr = read();
				if (curr == '=') {
					curr = read();
					return new Token("NE", "!=");
				}
			case 9:
				curr = read();
				if (isLetter(curr) || isNumeric(curr) || curr == '_') {
					stringBuffer += curr;
				} else {
					return new Token("ID", stringBuffer);
				}
			case 10:
				curr = read();
				if (!whiteSpace(curr)
						&& (isLetter(curr) || isNumeric(curr) || curr == '_')) {
					stringBuffer += curr;
					continue;
				} else {
					String val = keyWords.get(stringBuffer);
					if (val != null) {

						return new Token(val, stringBuffer);
					} else {
						// curr = read();
						return new Token("ID", stringBuffer);
					}
				}
			case 11:
				if (curr == '*') {
					state = 12;
					curr = read();
					continue;
				} else if (curr != '/' && !comment) {
					return new Token("DO", "/");
				} else {
					comment = true;
					if (curr != '\n' && curr != '\r' && curr != EOF) {
						curr = read();
						continue;
					} else {
						state = 1;
						continue;
					}

				}
			case 12:
				if (curr == '*') {
					curr = read();
					state = 13;
				} else {
					curr = read();
					continue;
				}
			case 13:
				if (curr == '/') {
					state = 1;
					curr = read();
					continue;
				} else {
					state = 12;
					curr = read();
				}

			}
		}
	}
}
